Skip to content

[miniflare] Add backend resources for email capture and storage - #15064

Open
tpmmorris wants to merge 10 commits into
cloudflare:mainfrom
tpmmorris:main
Open

[miniflare] Add backend resources for email capture and storage#15064
tpmmorris wants to merge 10 commits into
cloudflare:mainfrom
tpmmorris:main

Conversation

@tpmmorris

@tpmmorris tpmmorris commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Add backend resources for Email interaction within Local Explorer

Fixes #13648


Adds storage and capture methods for emails sent from/received by a worker using durable objects for storage (in line with the new 'Observability' tab), and cdn-cgi endpoints to mimic the sending of an email to a worker. Also records actions taken by the email() handler (received, forwarded, replied, rejected, unhandled), so that they can be mapped and displayed in local explorer in a similar manner as the dash. The Email result interface has been updated to also include a list of events.

  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: no changes are user-facing.

Open in Devin Review

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1291f71

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
miniflare Minor
wrangler Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@workers-devprod
workers-devprod requested review from a team and petebacondarwin and removed request for a team August 6, 2026 17:21
@workers-devprod

workers-devprod commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/local-email-capture.md: [@cloudflare/wrangler]
  • packages/miniflare/openapi-ts.config.ts: [@cloudflare/wrangler]
  • packages/miniflare/scripts/openapi-filter-config.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/explorer.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/temp-file.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/types.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/artifacts.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/store.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/email.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/entry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/capture.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email-store.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email-store.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/message-id.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/send_email.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/storage.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/validate.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/explorer.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/types.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/openapi.local.json: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/resources/email.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/route-names.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/index.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/email/artifacts.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/email/index.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/email.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/index.spec.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/createTestHarness.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/get-platform-proxy.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/multiworker-dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/test-harness.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/miniflare/src/workers/core/email.ts
rawBase64: bytesToBase64(rawEmailBuffer),
});

this.ctx.waitUntil(

@devin-ai-integration devin-ai-integration Bot Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Emails sent just before the dev session shuts down are never written to disk or logged

The on-disk copy of a sent email and its log line are queued to run in the background (this.ctx.waitUntil(...) at packages/miniflare/src/workers/email/send_email.worker.ts:370 and :451) instead of being finished before the send call returns, so a script that sends an email and then immediately shuts the local dev session down loses the saved message entirely.

Impact: Short-lived usages (for example sending through getPlatformProxy() and then disposing) no longer reliably produce the .eml/text/HTML/attachment files or the "send_email binding called..." log they used to.

Why the deferred work can be dropped

Before this change send() awaited every storeTempFile() call and logged before resolving, so by the time the caller's await env.SEND_EMAIL.send(...) returned the files existed. Now both branches resolve immediately after the in-workerd capture, deferring the loopback /core/store-temp-file writes and logging to ctx.waitUntil.

Miniflare#dispose() aborts, stops the loopback server and tears down workerd before drainEmailArtifactManager() runs (packages/miniflare/src/index.ts:3490-3496); drain() only awaits operations that already reached the Node side (packages/miniflare/src/plugins/email/artifacts.ts:86-89), so waitUntil work that has not yet issued its loopback request is simply discarded.

The test updates in this PR reflect the new asynchrony (the miniflare email specs now poll with vi.waitFor, and the get-platform-proxy e2e no longer asserts on the file), but callers that dispose right after sending have no way to wait.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread packages/miniflare/src/plugins/email/artifacts.ts Outdated
@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15064

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15064

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15064

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15064

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15064

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15064

miniflare

npm i https://pkg.pr.new/miniflare@15064

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15064

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15064

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15064

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15064

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@15064

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15064

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15064

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15064

wrangler

npm i https://pkg.pr.new/wrangler@15064

commit: 1291f71

devin-ai-integration[bot]

This comment was marked as resolved.

@tpmmorris
tpmmorris force-pushed the main branch 2 times, most recently from 7fccbb7 to 494e214 Compare August 7, 2026 12:57
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@petebacondarwin petebacondarwin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address the bugs highlighted by Devin

@github-project-automation github-project-automation Bot moved this from Untriaged to In Review in workers-sdk Aug 10, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@emily-shen emily-shen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a quick first pass on the API surface

"tags": ["Local Explorer"]
}
},
"/email/routing": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so because these have different semantics to the actual endpoints, we need to stick these in the local namespace. otherwise it will cause confusion between the 'real' api and this one

(https://developers.cloudflare.com/api/resources/email_routing/methods/get)

"schema": {
"type": "string"
},
"description": "Deliver the test email to this worker's email() handler, regardless of address-based routing."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by address-based routing? the port? that won't hold because there can be multiple workers per port. you could either use the worker name as part of the path or make this required

},
"outcome": {
"type": "string",
"enum": ["ok", "exception"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm. as in you would want a 200 exception if the handler intentionally threw?

task failed successfully i guess 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is a weird situation because the message was sent successfully, just the worker itself didn't like it. This is a confusing way to express it but I felt using a non 200 status would make it seem like the send itself failed. I'll make it clearer in the docs whats actually happening, unless youd have a different preference to how its handled?

"tags": ["Email"]
}
},
"/email/routing/{email_id}": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be a query param on GET /email/routing above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list and detail items have a different shape so i thought it would be appropriate to keep separate, if not I can make this change?

"tags": ["Email"]
}
},
"/email/sending/{email_id}": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly to above, query param on the main list endpoint?

Comment on lines +3721 to +3734
"properties": {
"type": {
"type": "string",
"enum": ["received", "forward", "reply", "reject", "unhandled"],
"description": "The kind of event."
},
"timestamp": {
"type": "string",
"description": "ISO 8601 timestamp of when the event occurred."
},
"messageId": {
"type": "string",
"description": "Present on `forward`/`reply` events; correlates with the matching `forwards`/`replies` entry."
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be a discriminated union to show that messageId is only available when the type is forward or reply

},
"headers": {
"type": "array",
"description": "Headers added to the forwarded message, as [key, value] pairs.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not an array of objects?

description:
"Worker whose email() handler processed the message, if known.",
},
from: { type: "string", description: "Envelope MAIL FROM address." },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's quite a lot of repetition in these objects with from/to/subject/messageId etc.

could you define an object like 'base_email' and extend that?

@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/local-email-capture.md: [@cloudflare/wrangler]
  • packages/miniflare/openapi-ts.config.ts: [@cloudflare/wrangler]
  • packages/miniflare/scripts/openapi-filter-config.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/explorer.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/temp-file.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/types.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/email/store.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/email.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/entry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/capture.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email-store.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email-store.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/email.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/message-id.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/send_email.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/storage.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/validate.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/explorer.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/types.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/openapi.local.json: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/resources/email.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/route-names.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/index.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/email/index.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/email.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/index.spec.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/createTestHarness.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/get-platform-proxy.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/multiworker-dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/test-harness.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@tpmmorris
tpmmorris marked this pull request as ready for review August 12, 2026 16:58
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +799 to +802
{
name: CoreBindings.TEXT_FALLBACK_WORKER_NAME,
json: JSON.stringify(fallbackWorkerPublicName ?? ""),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Fallback worker name setting is registered twice on the dev server, creating a conflicting duplicate

The entry worker's fallback-worker-name value is added a second time (serviceEntryBindings.push with json: JSON.stringify(fallbackWorkerPublicName ?? "") at packages/miniflare/src/plugins/core/index.ts:799-802) even though the same setting is already provided later in the same list, so the dev server ends up with two conflicting entries for one name.
Impact: The local dev server may refuse to start or silently pick the wrong value for which worker handles unrouted requests and received emails.

Duplicate MINIFLARE_FALLBACK_WORKER_NAME binding in serviceEntryBindings

getGlobalServices builds serviceEntryBindings for the SERVICE_ENTRY worker. The PR adds a new binding named CoreBindings.TEXT_FALLBACK_WORKER_NAME ("MINIFLARE_FALLBACK_WORKER_NAME") at packages/miniflare/src/plugins/core/index.ts:799-802 using a json value. However the pre-existing code already pushes a binding with the identical name at packages/miniflare/src/plugins/core/index.ts:928-931 using a text value (workerNames[0] ?? ""). Both live in the same serviceEntryBindings array, so the entry worker now declares two bindings with the same name. The entry worker Env type also now declares [CoreBindings.TEXT_FALLBACK_WORKER_NAME]: string twice (packages/miniflare/src/workers/core/entry.worker.ts:20 and :29), confirming the author did not notice the binding already existed. fallbackWorkerPublicName is set to this.#workerOpts[0].config.name (packages/miniflare/src/index.ts:2167), which equals workerNames[0], so the values coincide; the defect is the duplicated binding itself. The value feeds routeTarget in getTargetService (packages/miniflare/src/workers/core/entry.worker.ts:166), which is passed as workerName to handleEmail and used for the per-worker Access blob lookup. The correct fix is to remove one of the two bindings (keep a single definition).

Prompt for agents
In getGlobalServices (packages/miniflare/src/plugins/core/index.ts), the binding CoreBindings.TEXT_FALLBACK_WORKER_NAME is now pushed twice into serviceEntryBindings: once as a json binding near line 799-802 (value fallbackWorkerPublicName) and once as a text binding near line 928-931 (value workerNames[0]). This creates a duplicate binding name on the SERVICE_ENTRY worker. Consolidate these into a single binding definition (they carry the same first-worker name), and remove the now-redundant duplicate type entry in packages/miniflare/src/workers/core/entry.worker.ts (TEXT_FALLBACK_WORKER_NAME is declared twice in the Env type). Ensure the retained binding provides the value the entry worker expects for routeTarget/Access-blob lookup and the email workerName.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

4 participants